/**
 * Software License, Version 1.0 Copyright 2003 The Trustees of Indiana
 * University. All rights reserved. Redistribution and use in source and binary
 * forms, with or without modification, are permitted provided that the
 * following conditions are met: 1) All redistributions of source code must
 * retain the above copyright notice, the list of authors in the original source
 * code, this list of conditions and the disclaimer listed in this license; 2)
 * All redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the disclaimer listed in this license in the
 * documentation and/or other materials provided with the distribution; 3) Any
 * documentation included with all redistributions must include the following
 * acknowledgement: "This product includes software developed by the Community
 * Grids Lab. For further information contact the Community Grids Lab at
 * http://communitygrids.iu.edu/." Alternatively, this acknowledgement may
 * appear in the software itself, and wherever such third-party acknowledgments
 * normally appear. 4) The name Indiana University or Community Grids Lab or
 * NaradaBrokering, shall not be used to endorse or promote products derived
 * from this software without prior written permission from Indiana University.
 * For written permission, please contact the Advanced Research and Technology
 * Institute ("ARTI") at 351 West 10th Street, Indianapolis, Indiana 46202. 5)
 * Products derived from this software may not be called NaradaBrokering, nor
 * may Indiana University or Community Grids Lab or NaradaBrokering appear in
 * their name, without prior written permission of ARTI. Indiana University
 * provides no reassurances that the source code provided does not infringe the
 * patent or any other intellectual property rights of any other entity. Indiana
 * University disclaims any liability to any recipient for claims brought by any
 * other entity based on infringement of intellectual property rights or
 * otherwise. LICENSEE UNDERSTANDS THAT SOFTWARE IS PROVIDED "AS IS" FOR WHICH
 * NO WARRANTIES AS TO CAPABILITIES OR ACCURACY ARE MADE. INDIANA UNIVERSITY
 * GIVES NO WARRANTIES AND MAKES NO REPRESENTATION THAT SOFTWARE IS FREE OF
 * INFRINGEMENT OF THIRD PARTY PATENT, COPYRIGHT, OR OTHER PROPRIETARY RIGHTS.
 * INDIANA UNIVERSITY MAKES NO WARRANTIES THAT SOFTWARE IS FREE FROM "BUGS",
 * "VIRUSES", "TROJAN HORSES", "TRAP DOORS", "WORMS", OR OTHER HARMFUL CODE.
 * LICENSEE ASSUMES THE ENTIRE RISK AS TO THE PERFORMANCE OF SOFTWARE AND/OR
 * ASSOCIATED MATERIALS, AND TO THE PERFORMANCE AND VALIDITY OF INFORMATION
 * GENERATED USING SOFTWARE.
 */
package cgl.narada.service.security.securityprovider;

import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Security;
import java.security.SignatureException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.StringTokenizer;

import org.apache.log4j.Logger;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.DERInputStream;
import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.asn1.x509.SubjectKeyIdentifier;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.asn1.x509.X509Extensions;
import org.bouncycastle.asn1.x509.X509Name;
import org.bouncycastle.jce.PKCS10CertificationRequest;
import org.bouncycastle.jce.X509V3CertificateGenerator;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

/**
 * Certificate Signing Utility class. <BR>
 * <b>Acknowledgement: </B> Part of the jCertServices package
 * <p>
 * A Class to sign certificate requests. It uses the Legion of The Boucy Castle
 * JCE provider (jce-jdk13-117.jar) available at http://www.bouncycastle.org/
 * 
 * @author Vladimir Silva
 */

public class CertificateSigner {

	static {
		if (Security.getProvider("BC") == null)
			Security.insertProviderAt(new BouncyCastleProvider(), 0);
	}

	static Logger logger = Logger.getLogger("CertificateSigner");

	static X509V3CertificateGenerator _v3CertGenerator = new X509V3CertificateGenerator();

	/**
	 * CertSigner Constructor: Reads a cert request and signs it with a set of CA
	 * certs
	 * 
	 * @param certRqPath
	 *            Full path to the CSR (Cert request)
	 * @param caCertPath
	 *            Full path to the CA cert (PEM)
	 * @param caKeyPath
	 *            Full path to the CA key (PEM)
	 * @param caPwd
	 *            CA password
	 * @throws IOException
	 * @throws NoSuchAlgorithmException
	 * @throws NoSuchProviderException
	 * @throws InvalidKeyException
	 * @throws GeneralSecurityException
	 */
	public static X509Certificate signCertificate(CertificateManager certMan,
			String certRqPath, String caPwd) throws Exception {

		return signCertificate(certMan, new FileInputStream(certRqPath), caPwd);
	}

	/**
	 * Signs a Cert request (CSR)
	 * 
	 * @param inCertRq
	 *            CSR Input stream
	 * @param caPwd
	 *            CA password
	 * @throws IOException
	 * @throws NoSuchAlgorithmException
	 * @throws NoSuchProviderException
	 * @throws InvalidKeyException
	 * @throws GeneralSecurityException
	 */
	public static X509Certificate signCertificate(CertificateManager certMan,
			InputStream inCertRq, String caPwd) throws Exception {

		return loadKeysAndSign(certMan, inCertRq, caPwd);
	}

	/**
	 * Load A CSR and sign it using CA cert/key and password
	 * 
	 * @param inRq
	 *            CSR request input stream
	 * @param caPwd
	 *            CA key password
	 * @throws IOException
	 *             if an IO error occurs
	 * @throws NoSuchAlgorithmException
	 *             if Security error
	 * @throws NoSuchProviderException
	 * @throws InvalidKeyException
	 * @throws GeneralSecurityException
	 */
	private static X509Certificate loadKeysAndSign(CertificateManager certMan,
			InputStream inRq, String caPwd) throws InvalidKeyException,
			CertificateExpiredException, CertificateNotYetValidException,
			NoSuchAlgorithmException, NoSuchProviderException,
			SecurityException, SignatureException, CertificateException,
			IOException, GeneralSecurityException,
			java.security.GeneralSecurityException {

		byte[] derArray = CertificateUtil.readPEM(inRq,
				"-----BEGIN NEW CERTIFICATE REQUEST-----",
				"-----END NEW CERTIFICATE REQUEST-----");

		// extract pub key from rq
		PKCS10CertificationRequest rqDer = new PKCS10CertificationRequest(
				derArray);
		PublicKey rqPubKey = rqDer.getPublicKey();

		// 
		// X509Certificate caCert = CertUtil.loadCertificate(inCACert);
		// OpenSSLKey key = new BouncyCastleOpenSSLKey(inCAKey);

		X509Certificate caCert = (X509Certificate) CertificateUtil
				.getCertificate(certMan, certMan.ROOT_CA_ALIAS);
		PrivateKey caPrivKey = CertificateUtil.getPrivateKey(certMan,
				certMan.ROOT_CA_ALIAS);

		// Get request issuer
		String rqIssuer = rqDer.getCertificationRequestInfo().getSubject()
				.toString();

		// cert lifetime
		int months = 12;

		String alg = caCert.getSigAlgName();
		PublicKey caPubKey = caCert.getPublicKey();
		// String caIssuer =
		// decodeX509Subject(caCert.getSubjectDN().toString());
		String caIssuer = caCert.getSubjectDN().toString();

		// For soem reason the string gets reversed i.e. C first and ending with
		// CN. This causes matching to fail. So reverse the string so that CN is
		// first and C last.

		String revCA = "";
		StringTokenizer st = new StringTokenizer(caIssuer, ",");

		while (st.hasMoreTokens()) {
			revCA = st.nextToken() + "," + revCA;
		}
		caIssuer = revCA.substring(0, revCA.length() - 1);

		// TODO:Needs to be unique...
		long serial = 0;

		logger.info("Constructor: Request Info - Issuer=" + rqIssuer + " alg="
				+ alg + " lifetime(months)=" + months);
		logger.info("Constructor: CA Info - Priv key serial=" + serial
				+ " Issuer=" + caIssuer);

		// sign
		return signCert(rqPubKey, caPubKey, caPrivKey, caIssuer, rqIssuer,
				serial, months, alg);

	}

	/**
	 * Save a signed certificate (PEM encoded)
	 * 
	 * @param os
	 *            Stream where the cert is to be saved
	 * @throws IOException
	 *             if a save error occurs
	 * @throws CertificateEncodingException
	 *             if a PEM encoding error occurs
	 */
	public static void save(X509Certificate _signedCert, OutputStream os)
			throws IOException, CertificateEncodingException {
		String s = CertificateUtil.writePEM(_signedCert.getEncoded(),
				"-----BEGIN CERTIFICATE-----\n", "-----END CERTIFICATE-----\n");
		logger.debug("save: Signed cert PEM\n" + s);
		os.write(s.getBytes());
	}

	/**
	 * Sign a certificate
	 * 
	 * @param rqPubKey
	 *            USer request public key
	 * @param caPubKey
	 *            Certificate authority (CA) pub key
	 * @param caPrivKey
	 *            CA private key
	 * @param caIssuer
	 *            CA Issuer string (e.g /O=Grid/O=Globus/OU=simpleCA....
	 * @param rqIssuer
	 *            Request issuer
	 * @param serial
	 *            Private key serial number
	 * @param lifeTimeMonths
	 *            Cert lifetime in months
	 * @param signAlgoritm
	 *            Signature Algorithm (e.g sha1WithEncryption)
	 * @return signed X509Certificate
	 * @throws GeneralSecurityException
	 * @throws IOException
	 */
	private static X509Certificate signCert(PublicKey rqPubKey,
			PublicKey caPubKey, PrivateKey caPrivKey, String caIssuer,
			String rqIssuer, long serial, int lifeTimeMonths,
			String signAlgoritm) throws InvalidKeyException,
			CertificateExpiredException, CertificateNotYetValidException,
			NoSuchProviderException, SecurityException, SignatureException,
			CertificateException, NoSuchAlgorithmException,
			GeneralSecurityException, IOException, InvalidKeyException {

		_v3CertGenerator.reset();

		_v3CertGenerator.setSerialNumber(BigInteger.valueOf(serial));
		_v3CertGenerator.setIssuerDN(new X509Name(caIssuer));
		_v3CertGenerator.setNotBefore(new Date(System.currentTimeMillis()));
		_v3CertGenerator.setNotAfter(new Date(System.currentTimeMillis()
				+ lifeTimeMonths * (1000L * 60 * 60 * 24 * 30)));
		_v3CertGenerator.setSubjectDN(new X509Name(rqIssuer));
		_v3CertGenerator.setPublicKey(rqPubKey);
		_v3CertGenerator.setSignatureAlgorithm(signAlgoritm);

		// cert extensions

		_v3CertGenerator.addExtension(X509Extensions.SubjectKeyIdentifier,
				false, createSubjectKeyId(rqPubKey));
		_v3CertGenerator.addExtension(X509Extensions.AuthorityKeyIdentifier,
				false, createAuthorityKeyId(caPubKey));

		_v3CertGenerator.addExtension(X509Extensions.BasicConstraints, false,
				new BasicConstraints(false));

		_v3CertGenerator.addExtension(X509Extensions.KeyUsage, false,
				new KeyUsage(KeyUsage.dataEncipherment
						| KeyUsage.digitalSignature));

		X509Certificate cert;

		cert = _v3CertGenerator.generateX509Certificate(caPrivKey, "BC",
				new java.security.SecureRandom());

		cert.checkValidity(new Date());
		cert.verify(caPubKey);

		return cert;
	}

	/**
	 * @param pubKey
	 * @return
	 */
	private static SubjectKeyIdentifier createSubjectKeyId(PublicKey pubKey) {
		try {
			ByteArrayInputStream bIn = new ByteArrayInputStream(pubKey
					.getEncoded());

			SubjectPublicKeyInfo info = new SubjectPublicKeyInfo(
					(ASN1Sequence) new DERInputStream(bIn).readObject());

			return new SubjectKeyIdentifier(info);
		}
		catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}

	/**
	 * @param pubKey
	 * @param name
	 * @param sNumber
	 * @return
	 * @throws IOException
	 */
	private static AuthorityKeyIdentifier createAuthorityKeyId(
			PublicKey pubKey, X509Name name, int sNumber) throws IOException {
		ByteArrayInputStream bIn = new ByteArrayInputStream(pubKey.getEncoded());
		SubjectPublicKeyInfo info = new SubjectPublicKeyInfo(
				(ASN1Sequence) new DERInputStream(bIn).readObject());

		GeneralName genName = new GeneralName(name);

		// DERConstructedSequence seq = new DERConstructedSequence();
		ASN1Sequence seq = ASN1Sequence.getInstance(genName);

		// seq.addObject(genName);

		return new AuthorityKeyIdentifier(info, new GeneralNames(seq),
				BigInteger.valueOf(sNumber));
	}

	/**
	 * @param pubKey
	 * @return
	 * @throws IOException
	 */
	private static AuthorityKeyIdentifier createAuthorityKeyId(PublicKey pubKey)
			throws IOException {
		ByteArrayInputStream bIn = new ByteArrayInputStream(pubKey.getEncoded());
		SubjectPublicKeyInfo info = new SubjectPublicKeyInfo(
				(ASN1Sequence) new DERInputStream(bIn).readObject());
		return new AuthorityKeyIdentifier(info);
	}

	// For debuging only!
	// public static void main(String[] args) {
	// try {
	// Logger.getRootLogger().setLevel(Level.DEBUG);
	// String rqPath = "C:\\Documents and
	// Settings\\Administrator\\.globus\\usercert_request.pem";
	// String userKeyPath = "C:\\Documents and
	// Settings\\Administrator\\.globus\\userkey.pem";
	// String caCertPath = "C:\\Documents and
	// Settings\\Administrator\\.globus\\simpleCA\\cacert.pem";
	// String caKeyPath = "C:\\Documents and
	// Settings\\Administrator\\.globus\\simpleCA\\cakey.pem";
	// String signedCertPath = "C:\\Documents and
	// Settings\\Administrator\\.globus\\usercert.pem";
	// String subject =
	// "O=Grid,OU=GlobusTest,OU=simpleCA-vladimir.pok.ibm.com,OU=pok.ibm.com,CN=vsilva";
	//         
	// String subject = GSIProperties
	// .getString(GSIProperties.LOCAL_USER_SUBJECT)
	// + ",CN=Vladimir Silva";
	// CertGenerator generator = new CertGenerator(subject);
	// CertManager mgr = new CertManager(generator);
	// mgr.createCertRequest(1024, "2p2dkdt");
	// mgr.saveCertRequest(rqPath, userKeyPath); // sign rq CertSigner signer
	// // =
	// new CertSigner(rqPath, caCertPath, caKeyPath, "2p2dkdt");
	// signer.save(new FileOutputStream(signedCertPath));
	// logger.debug("CA Cert\n"
	// + CertUtil.loadCertificate(GSIProperties._defCACert)
	// .toString());
	// } catch (Exception ex) {
	// ex.printStackTrace();
	// }
	// }
}